import { useCallback, useEffect, useMemo, useState } from 'react';
import dayjs from 'dayjs';
import { uniqBy } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { useContext } from 'use-context-selector';
import { getIncomers, getOutgoers, useStoreApi } from 'reactflow';
import type { Connection } from 'reactflow';
import type { Edge, Node, ValueSelector } from '../types';
import { BlockEnum, WorkflowRunningStatus } from '../types';
import { useStore, useWorkflowStore } from '../store';
import { getParallelInfo } from '../utils';
import { PARALLEL_DEPTH_LIMIT, PARALLEL_LIMIT, SUPPORT_OUTPUT_VARS_NODE } from '../constants';
import { CUSTOM_NOTE_NODE } from '../note-node/constants';
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils';
import { useNodesExtraData } from './use-nodes-data';
import { useWorkflowTemplate } from './use-workflow-template';
import { useStore as useAppStore } from '@/app/components/app/store';
import { fetchNodesDefaultConfigs, fetchPublishedWorkflow, fetchWorkflowDraft, syncWorkflowDraft } from '@/service/workflow';
import type { FetchWorkflowDraftResponse } from '@/types/workflow';
import { fetchAllBuiltInTools, fetchAllCustomTools, fetchAllWorkflowTools } from '@/service/tools';
import I18n from '@/context/i18n';
import { CollectionType } from '@/app/components/tools/types';
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants';

export const useIsChatMode = () => {
  const appDetail = useAppStore((s) => s.appDetail);

  return appDetail?.mode === 'advanced-chat';
};

export const useWorkflow = () => {
  const { t } = useTranslation();
  const { locale } = useContext(I18n);
  const store = useStoreApi();
  const workflowStore = useWorkflowStore();
  const nodesExtraData = useNodesExtraData();
  const setPanelWidth = useCallback(
    (width: number) => {
      localStorage.setItem('workflow-node-panel-width', `${width}`);
      workflowStore.setState({ panelWidth: width });
    },
    [workflowStore],
  );

  const getTreeLeafNodes = useCallback(
    (nodeId: string) => {
      const { getNodes, edges } = store.getState();
      const nodes = getNodes();
      let startNode = nodes.find((node) => node.data.type === BlockEnum.Start);
      const currentNode = nodes.find((node) => node.id === nodeId);

      if (currentNode?.parentId)
        startNode = nodes.find((node) => node.parentId === currentNode.parentId && node.type === CUSTOM_ITERATION_START_NODE);

      if (!startNode) return [];

      const list: Node[] = [];
      const preOrder = (root: Node, callback: (node: Node) => void) => {
        if (root.id === nodeId) return;
        const outgoers = getOutgoers(root, nodes, edges);

        if (outgoers.length) {
          outgoers.forEach((outgoer) => {
            preOrder(outgoer, callback);
          });
        } else {
          if (root.id !== nodeId) callback(root);
        }
      };
      preOrder(startNode, (node) => {
        list.push(node);
      });

      const incomers = getIncomers({ id: nodeId } as Node, nodes, edges);

      list.push(...incomers);

      return uniqBy(list, 'id').filter((item) => {
        return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type);
      });
    },
    [store],
  );

  const getBeforeNodesInSameBranch = useCallback(
    (nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
      const { getNodes, edges } = store.getState();
      const nodes = newNodes || getNodes();
      const currentNode = nodes.find((node) => node.id === nodeId);

      const list: Node[] = [];

      if (!currentNode) return list;

      if (currentNode.parentId) {
        const parentNode = nodes.find((node) => node.id === currentNode.parentId);
        if (parentNode) {
          const parentList = getBeforeNodesInSameBranch(parentNode.id);

          list.push(...parentList);
        }
      }

      const traverse = (root: Node, callback: (node: Node) => void) => {
        if (root) {
          const incomers = getIncomers(root, nodes, newEdges || edges);

          if (incomers.length) {
            incomers.forEach((node) => {
              if (!list.find((n) => node.id === n.id)) {
                callback(node);
                traverse(node, callback);
              }
            });
          }
        }
      };
      traverse(currentNode, (node) => {
        list.push(node);
      });

      const length = list.length;
      if (length) {
        return uniqBy(list, 'id')
          .reverse()
          .filter((item) => {
            return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type);
          });
      }

      return [];
    },
    [store],
  );

  const getBeforeNodesInSameBranchIncludeParent = useCallback(
    (nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
      const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges);
      const { getNodes } = store.getState();
      const allNodes = getNodes();
      const node = allNodes.find((n) => n.id === nodeId);
      const parentNodeId = node?.parentId;
      const parentNode = allNodes.find((n) => n.id === parentNodeId);
      if (parentNode) nodes.push(parentNode);

      return nodes;
    },
    [getBeforeNodesInSameBranch, store],
  );

  const getAfterNodesInSameBranch = useCallback(
    (nodeId: string) => {
      const { getNodes, edges } = store.getState();
      const nodes = getNodes();
      const currentNode = nodes.find((node) => node.id === nodeId)!;

      if (!currentNode) return [];
      const list: Node[] = [currentNode];

      const traverse = (root: Node, callback: (node: Node) => void) => {
        if (root) {
          const outgoers = getOutgoers(root, nodes, edges);

          if (outgoers.length) {
            outgoers.forEach((node) => {
              callback(node);
              traverse(node, callback);
            });
          }
        }
      };
      traverse(currentNode, (node) => {
        list.push(node);
      });

      return uniqBy(list, 'id');
    },
    [store],
  );

  const getBeforeNodeById = useCallback(
    (nodeId: string) => {
      const { getNodes, edges } = store.getState();
      const nodes = getNodes();
      const node = nodes.find((node) => node.id === nodeId)!;

      return getIncomers(node, nodes, edges);
    },
    [store],
  );

  const getIterationNodeChildren = useCallback(
    (nodeId: string) => {
      const { getNodes } = store.getState();
      const nodes = getNodes();

      return nodes.filter((node) => node.parentId === nodeId);
    },
    [store],
  );

  const handleOutVarRenameChange = useCallback(
    (nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
      const { getNodes, setNodes } = store.getState();
      const afterNodes = getAfterNodesInSameBranch(nodeId);
      const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes);
      if (effectNodes.length > 0) {
        const newNodes = getNodes().map((node) => {
          if (effectNodes.find((n) => n.id === node.id)) return updateNodeVars(node, oldValeSelector, newVarSelector);

          return node;
        });
        setNodes(newNodes);
      }

      // eslint-disable-next-line react-hooks/exhaustive-deps
    },
    [store],
  );

  const isVarUsedInNodes = useCallback(
    (varSelector: ValueSelector) => {
      const nodeId = varSelector[0];
      const afterNodes = getAfterNodesInSameBranch(nodeId);
      const effectNodes = findUsedVarNodes(varSelector, afterNodes);
      return effectNodes.length > 0;
    },
    [getAfterNodesInSameBranch],
  );

  const removeUsedVarInNodes = useCallback(
    (varSelector: ValueSelector) => {
      const nodeId = varSelector[0];
      const { getNodes, setNodes } = store.getState();
      const afterNodes = getAfterNodesInSameBranch(nodeId);
      const effectNodes = findUsedVarNodes(varSelector, afterNodes);
      if (effectNodes.length > 0) {
        const newNodes = getNodes().map((node) => {
          if (effectNodes.find((n) => n.id === node.id)) return updateNodeVars(node, varSelector, []);

          return node;
        });
        setNodes(newNodes);
      }
    },
    [getAfterNodesInSameBranch, store],
  );

  const isNodeVarsUsedInNodes = useCallback(
    (node: Node, isChatMode: boolean) => {
      const outputVars = getNodeOutputVars(node, isChatMode);
      const isUsed = outputVars.some((varSelector) => {
        return isVarUsedInNodes(varSelector);
      });
      return isUsed;
    },
    [isVarUsedInNodes],
  );

  const checkParallelLimit = useCallback(
    (nodeId: string, nodeHandle = 'source') => {
      const { edges } = store.getState();
      const connectedEdges = edges.filter((edge) => edge.source === nodeId && edge.sourceHandle === nodeHandle);
      if (connectedEdges.length > PARALLEL_LIMIT - 1) {
        const { setShowTips } = workflowStore.getState();
        setShowTips(t('workflow.common.parallelTip.limit', { num: PARALLEL_LIMIT }));
        return false;
      }

      return true;
    },
    [store, workflowStore, t],
  );

  const checkNestedParallelLimit = useCallback(
    (nodes: Node[], edges: Edge[], parentNodeId?: string) => {
      const { parallelList, hasAbnormalEdges } = getParallelInfo(nodes, edges, parentNodeId);

      if (hasAbnormalEdges) return false;

      for (let i = 0; i < parallelList.length; i++) {
        const parallel = parallelList[i];

        if (parallel.depth > PARALLEL_DEPTH_LIMIT) {
          const { setShowTips } = workflowStore.getState();
          setShowTips(t('workflow.common.parallelTip.depthLimit', { num: PARALLEL_DEPTH_LIMIT }));
          return false;
        }
      }

      return true;
    },
    [t, workflowStore],
  );

  const isValidConnection = useCallback(
    ({ source, sourceHandle, target }: Connection) => {
      const { edges, getNodes } = store.getState();
      const nodes = getNodes();
      const sourceNode: Node = nodes.find((node) => node.id === source)!;
      const targetNode: Node = nodes.find((node) => node.id === target)!;

      if (!checkParallelLimit(source!, sourceHandle || 'source')) return false;

      if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE) return false;

      if (sourceNode.parentId !== targetNode.parentId) return false;

      if (sourceNode && targetNode) {
        const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes;
        const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start];

        if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) return false;

        if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type)) return false;
      }

      const hasCycle = (node: Node, visited = new Set()) => {
        if (visited.has(node.id)) return false;

        visited.add(node.id);

        for (const outgoer of getOutgoers(node, nodes, edges)) {
          if (outgoer.id === source) return true;
          if (hasCycle(outgoer, visited)) return true;
        }
      };

      return !hasCycle(targetNode);
    },
    [store, nodesExtraData, checkParallelLimit],
  );

  const formatTimeFromNow = useCallback(
    (time: number) => {
      return dayjs(time)
        .locale(locale === 'zh-Hans' ? 'zh-cn' : locale)
        .fromNow();
    },
    [locale],
  );

  const getNode = useCallback(
    (nodeId?: string) => {
      const { getNodes } = store.getState();
      const nodes = getNodes();

      return nodes.find((node) => node.id === nodeId) || nodes.find((node) => node.data.type === BlockEnum.Start);
    },
    [store],
  );

  return {
    setPanelWidth,
    getTreeLeafNodes,
    getBeforeNodesInSameBranch,
    getBeforeNodesInSameBranchIncludeParent,
    getAfterNodesInSameBranch,
    handleOutVarRenameChange,
    isVarUsedInNodes,
    removeUsedVarInNodes,
    isNodeVarsUsedInNodes,
    checkParallelLimit,
    checkNestedParallelLimit,
    isValidConnection,
    formatTimeFromNow,
    getNode,
    getBeforeNodeById,
    getIterationNodeChildren,
  };
};

export const useFetchToolsData = () => {
  const workflowStore = useWorkflowStore();

  const handleFetchAllTools = useCallback(
    async (type: string) => {
      if (type === 'builtin') {
        const buildInTools = await fetchAllBuiltInTools();

        workflowStore.setState({
          buildInTools: buildInTools || [],
        });
      }
      if (type === 'custom') {
        const customTools = await fetchAllCustomTools();

        workflowStore.setState({
          customTools: customTools || [],
        });
      }
      if (type === 'workflow') {
        const workflowTools = await fetchAllWorkflowTools();

        workflowStore.setState({
          workflowTools: workflowTools || [],
        });
      }
    },
    [workflowStore],
  );

  return {
    handleFetchAllTools,
  };
};

export const useWorkflowInit = () => {
  const workflowStore = useWorkflowStore();
  const { nodes: nodesTemplate, edges: edgesTemplate } = useWorkflowTemplate();
  const { handleFetchAllTools } = useFetchToolsData();
  const appDetail = useAppStore((state) => state.appDetail)!;
  const setSyncWorkflowDraftHash = useStore((s) => s.setSyncWorkflowDraftHash);
  const [data, setData] = useState<FetchWorkflowDraftResponse>();
  const [isLoading, setIsLoading] = useState(true);
  workflowStore.setState({ appId: appDetail.id });

  const handleGetInitialWorkflowData = useCallback(async () => {
    try {
      const res = await fetchWorkflowDraft(`/workflows/draft?id=${appDetail.id}`);
      setData(res);
      workflowStore.setState({
        envSecrets: (res.environment_variables || [])
          .filter((env) => env.value_type === 'secret')
          .reduce(
            (acc, env) => {
              acc[env.id] = env.value;
              return acc;
            },
            {} as Record<string, string>,
          ),
        environmentVariables:
          res.environment_variables?.map((env) => (env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env)) || [],
        // #TODO chatVar sync#
        conversationVariables: res.conversation_variables || [],
      });
      setSyncWorkflowDraftHash(res.hash);
      setIsLoading(false);
    } catch (error: any) {
      if (error && error.json && !error.bodyUsed && appDetail) {
        error.json().then((err: any) => {
          if (err.code === 'draft_workflow_not_exist') {
            workflowStore.setState({ notInitialWorkflow: true });
            syncWorkflowDraft({
              url: `/workflows/draft?id=${appDetail.id}`,
              params: {
                graph: {
                  nodes: nodesTemplate,
                  edges: edgesTemplate,
                },
                features: {
                  retriever_resource: { enabled: true },
                },
                environment_variables: [],
                conversation_variables: [],
              },
            }).then((res) => {
              workflowStore.getState().setDraftUpdatedAt(res.updated_at);
              handleGetInitialWorkflowData();
            });
          }
        });
      }
    }
  }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]);

  useEffect(() => {
    handleGetInitialWorkflowData();
  }, []);

  const handleFetchPreloadData = useCallback(async () => {
    try {
      // const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
      // const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
      const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/workflows/default-workflow-block-configs?id=${appDetail?.id}`);
      const publishedWorkflow = await fetchPublishedWorkflow(`/workflows/publish?id=${appDetail?.id}`);
      workflowStore.setState({
        nodesDefaultConfigs: nodesDefaultConfigsData.reduce(
          (acc, block) => {
            if (!acc[block.type]) acc[block.type] = { ...block.config };
            return acc;
          },
          {} as Record<string, any>,
        ),
      });
      workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at);
    } catch (e) {}
  }, [workflowStore, appDetail]);

  useEffect(() => {
    handleFetchPreloadData();
    handleFetchAllTools('builtin');
    handleFetchAllTools('custom');
    handleFetchAllTools('workflow');
  }, [handleFetchPreloadData, handleFetchAllTools]);

  useEffect(() => {
    if (data) {
      workflowStore.getState().setDraftUpdatedAt(data.updated_at);
      workflowStore.getState().setToolPublished(data.tool_published);
    }
  }, [data, workflowStore]);

  return {
    data,
    isLoading,
  };
};

export const useWorkflowReadOnly = () => {
  const workflowStore = useWorkflowStore();
  const workflowRunningData = useStore((s) => s.workflowRunningData);

  const getWorkflowReadOnly = useCallback(() => {
    return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running;
  }, [workflowStore]);

  return {
    workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
    getWorkflowReadOnly,
  };
};
export const useNodesReadOnly = () => {
  const workflowStore = useWorkflowStore();
  const workflowRunningData = useStore((s) => s.workflowRunningData);
  const historyWorkflowData = useStore((s) => s.historyWorkflowData);
  const isRestoring = useStore((s) => s.isRestoring);

  const getNodesReadOnly = useCallback(() => {
    const { workflowRunningData, historyWorkflowData, isRestoring } = workflowStore.getState();

    return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring;
  }, [workflowStore]);

  return {
    nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
    getNodesReadOnly,
  };
};

export const useToolIcon = (data: Node['data']) => {
  const buildInTools = useStore((s) => s.buildInTools);
  const customTools = useStore((s) => s.customTools);
  const workflowTools = useStore((s) => s.workflowTools);
  const toolIcon = useMemo(() => {
    if (data.type === BlockEnum.Tool) {
      let targetTools = buildInTools;
      if (data.provider_type === CollectionType.builtIn) targetTools = buildInTools;
      else if (data.provider_type === CollectionType.custom) targetTools = customTools;
      else targetTools = workflowTools;
      return targetTools.find((toolWithProvider) => toolWithProvider.id === data.provider_id)?.icon;
    }
  }, [data, buildInTools, customTools, workflowTools]);

  return toolIcon;
};

export const useIsNodeInIteration = (iterationId: string) => {
  const store = useStoreApi();

  const isNodeInIteration = useCallback(
    (nodeId: string) => {
      const { getNodes } = store.getState();
      const nodes = getNodes();
      const node = nodes.find((node) => node.id === nodeId);

      if (!node) return false;

      if (node.parentId === iterationId) return true;

      return false;
    },
    [iterationId, store],
  );
  return {
    isNodeInIteration,
  };
};
